/*
 * Copyright (c) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */


import { unzlibSync } from "./fflate.js";
import { globalObject } from "./globalObject.js";

var PNG = (function() {
  var APNG_BLEND_OP_OVER,
    APNG_BLEND_OP_SOURCE,
    APNG_DISPOSE_OP_BACKGROUND,
    APNG_DISPOSE_OP_NONE,
    APNG_DISPOSE_OP_PREVIOUS,
    makeImage,
    scratchCanvas,
    scratchCtx;

  APNG_DISPOSE_OP_NONE = 0;

  APNG_DISPOSE_OP_BACKGROUND = 1;

  APNG_DISPOSE_OP_PREVIOUS = 2;

  APNG_BLEND_OP_SOURCE = 0;

  APNG_BLEND_OP_OVER = 1;

  function PNG(data) {
    var chunkSize,
      colors,
      palLen,
      delayDen,
      delayNum,
      frame,
      i,
      index,
      key,
      section,
      palShort,
      text,
      _i,
      _j,
      _ref;
    this.data = data;
    this.pos = 8;
    this.palette = [];
    this.imgData = [];
    this.transparency = {};
    this.animation = null;
    this.text = {};
    frame = null;
    while (true) {
      chunkSize = this.readUInt32();
      section = function() {
        var _i, _results;
        _results = [];
        for (i = _i = 0; _i < 4; i = ++_i) {
          _results.push(String.fromCharCode(this.data[this.pos++]));
        }
        return _results;
      }
        .call(this)
        .join("");
      switch (section) {
        case "IHDR":
          this.width = this.readUInt32();
          this.height = this.readUInt32();
          this.bits = this.data[this.pos++];
          this.colorType = this.data[this.pos++];
          this.compressionMethod = this.data[this.pos++];
          this.filterMethod = this.data[this.pos++];
          this.interlaceMethod = this.data[this.pos++];
          break;
        case "acTL":
          this.animation = {
            numFrames: this.readUInt32(),
            numPlays: this.readUInt32() || Infinity,
            frames: []
          };
          break;
        case "PLTE":
          this.palette = this.read(chunkSize);
          break;
        case "fcTL":
          if (frame) {
            this.animation.frames.push(frame);
          }
          this.pos += 4;
          frame = {
            width: this.readUInt32(),
            height: this.readUInt32(),
            xOffset: this.readUInt32(),
            yOffset: this.readUInt32()
          };
          delayNum = this.readUInt16();
          delayDen = this.readUInt16() || 100;
          frame.delay = (1000 * delayNum) / delayDen;
          frame.disposeOp = this.data[this.pos++];
          frame.blendOp = this.data[this.pos++];
          frame.data = [];
          break;
        case "IDAT":
        case "fdAT":
          if (section === "fdAT") {
            this.pos += 4;
            chunkSize -= 4;
          }
          data = (frame != null ? frame.data : void 0) || this.imgData;
          for (
            i = _i = 0;
            0 <= chunkSize ? _i < chunkSize : _i > chunkSize;
            i = 0 <= chunkSize ? ++_i : --_i
          ) {
            data.push(this.data[this.pos++]);
          }
          break;
        case "tRNS":
          this.transparency = {};
          switch (this.colorType) {
            case 3:
              palLen = this.palette.length / 3;
              this.transparency.indexed = this.read(chunkSize);
              if (this.transparency.indexed.length > palLen)
                throw new Error("More transparent colors than palette size");
              /*
               * According to the PNG spec trns should be increased to the same size as palette if shorter
               */
              //palShort = 255 - this.transparency.indexed.length;
              palShort = palLen - this.transparency.indexed.length;
              if (palShort > 0) {
                for (
                  i = _j = 0;
                  0 <= palShort ? _j < palShort : _j > palShort;
                  i = 0 <= palShort ? ++_j : --_j
                ) {
                  this.transparency.indexed.push(255);
                }
              }
              break;
            case 0:
              this.transparency.grayscale = this.read(chunkSize)[0];
              break;
            case 2:
              this.transparency.rgb = this.read(chunkSize);
          }
          break;
        case "tEXt":
          text = this.read(chunkSize);
          index = text.indexOf(0);
          key = String.fromCharCode.apply(String, text.slice(0, index));
          this.text[key] = String.fromCharCode.apply(
            String,
            text.slice(index + 1)
          );
          break;
        case "IEND":
          if (frame) {
            this.animation.frames.push(frame);
          }
          this.colors = function() {
            switch (this.colorType) {
              case 0:
              case 3:
              case 4:
                return 1;
              case 2:
              case 6:
                return 3;
            }
          }.call(this);
          this.hasAlphaChannel = (_ref = this.colorType) === 4 || _ref === 6;
          colors = this.colors + (this.hasAlphaChannel ? 1 : 0);
          this.pixelBitlength = this.bits * colors;
          this.colorSpace = function() {
            switch (this.colors) {
              case 1:
                return "DeviceGray";
              case 3:
                return "DeviceRGB";
            }
          }.call(this);
          this.imgData = new Uint8Array(this.imgData);
          return;
        default:
          this.pos += chunkSize;
      }
      this.pos += 4;
      if (this.pos > this.data.length) {
        throw new Error("Incomplete or corrupt PNG file");
      }
    }
  }

  PNG.prototype.read = function(bytes) {
    var i, _i, _results;
    _results = [];
    for (
      i = _i = 0;
      0 <= bytes ? _i < bytes : _i > bytes;
      i = 0 <= bytes ? ++_i : --_i
    ) {
      _results.push(this.data[this.pos++]);
    }
    return _results;
  };

  PNG.prototype.readUInt32 = function() {
    var b1, b2, b3, b4;
    b1 = this.data[this.pos++] << 24;
    b2 = this.data[this.pos++] << 16;
    b3 = this.data[this.pos++] << 8;
    b4 = this.data[this.pos++];
    return b1 | b2 | b3 | b4;
  };

  PNG.prototype.readUInt16 = function() {
    var b1, b2;
    b1 = this.data[this.pos++] << 8;
    b2 = this.data[this.pos++];
    return b1 | b2;
  };

  PNG.prototype.decodePixels = function(data) {
    var pixelBytes = this.pixelBitlength / 8;
    var fullPixels = new Uint8Array(this.width * this.height * pixelBytes);
    var pos = 0;
    var _this = this;

    if (data == null) {
      data = this.imgData;
    }
    if (data.length === 0) {
      return new Uint8Array(0);
    }

    data = unzlibSync(data);
    function pass(x0, y0, dx, dy) {
      var abyte,
        c,
        col,
        i,
        left,
        length,
        p,
        pa,
        paeth,
        pb,
        pc,
        pixels,
        row,
        scanlineLength,
        upper,
        upperLeft,
        _i,
        _j,
        _k,
        _l,
        _m;
      var w = Math.ceil((_this.width - x0) / dx),
        h = Math.ceil((_this.height - y0) / dy);
      var isFull = _this.width == w && _this.height == h;
      scanlineLength = pixelBytes * w;
      pixels = isFull ? fullPixels : new Uint8Array(scanlineLength * h);
      length = data.length;
      row = 0;
      c = 0;
      while (row < h && pos < length) {
        switch (data[pos++]) {
          case 0:
            for (i = _i = 0; _i < scanlineLength; i = _i += 1) {
              pixels[c++] = data[pos++];
            }
            break;
          case 1:
            for (i = _j = 0; _j < scanlineLength; i = _j += 1) {
              abyte = data[pos++];
              left = i < pixelBytes ? 0 : pixels[c - pixelBytes];
              pixels[c++] = (abyte + left) % 256;
            }
            break;
          case 2:
            for (i = _k = 0; _k < scanlineLength; i = _k += 1) {
              abyte = data[pos++];
              col = (i - (i % pixelBytes)) / pixelBytes;
              upper =
                row &&
                pixels[
                  (row - 1) * scanlineLength +
                    col * pixelBytes +
                    (i % pixelBytes)
                ];
              pixels[c++] = (upper + abyte) % 256;
            }
            break;
          case 3:
            for (i = _l = 0; _l < scanlineLength; i = _l += 1) {
              abyte = data[pos++];
              col = (i - (i % pixelBytes)) / pixelBytes;
              left = i < pixelBytes ? 0 : pixels[c - pixelBytes];
              upper =
                row &&
                pixels[
                  (row - 1) * scanlineLength +
                    col * pixelBytes +
                    (i % pixelBytes)
                ];
              pixels[c++] = (abyte + Math.floor((left + upper) / 2)) % 256;
            }
            break;
          case 4:
            for (i = _m = 0; _m < scanlineLength; i = _m += 1) {
              abyte = data[pos++];
              col = (i - (i % pixelBytes)) / pixelBytes;
              left = i < pixelBytes ? 0 : pixels[c - pixelBytes];
              if (row === 0) {
                upper = upperLeft = 0;
              } else {
                upper =
                  pixels[
                    (row - 1) * scanlineLength +
                      col * pixelBytes +
                      (i % pixelBytes)
                  ];
                upperLeft =
                  col &&
                  pixels[
                    (row - 1) * scanlineLength +
                      (col - 1) * pixelBytes +
                      (i % pixelBytes)
                  ];
              }
              p = left + upper - upperLeft;
              pa = Math.abs(p - left);
              pb = Math.abs(p - upper);
              pc = Math.abs(p - upperLeft);
              if (pa <= pb && pa <= pc) {
                paeth = left;
              } else if (pb <= pc) {
                paeth = upper;
              } else {
                paeth = upperLeft;
              }
              pixels[c++] = (abyte + paeth) % 256;
            }
            break;
          default:
            throw new Error("Invalid filter algorithm: " + data[pos - 1]);
        }
        if (!isFull) {
          var fullPos = ((y0 + row * dy) * _this.width + x0) * pixelBytes;
          var partPos = row * scanlineLength;
          for (i = 0; i < w; i += 1) {
            for (var j = 0; j < pixelBytes; j += 1)
              fullPixels[fullPos++] = pixels[partPos++];
            fullPos += (dx - 1) * pixelBytes;
          }
        }
        row++;
      }
    }
    if (_this.interlaceMethod == 1) {
      /*
          1 6 4 6 2 6 4 6
          7 7 7 7 7 7 7 7
          5 6 5 6 5 6 5 6
          7 7 7 7 7 7 7 7
          3 6 4 6 3 6 4 6
          7 7 7 7 7 7 7 7
          5 6 5 6 5 6 5 6
          7 7 7 7 7 7 7 7
        */
      pass(0, 0, 8, 8); // 1
      /* NOTE these seem to follow the pattern:
       * pass(x, 0, 2*x, 2*x);
       * pass(0, x,   x, 2*x);
       * with x being 4, 2, 1.
       */
      pass(4, 0, 8, 8); // 2
      pass(0, 4, 4, 8); // 3

      pass(2, 0, 4, 4); // 4
      pass(0, 2, 2, 4); // 5

      pass(1, 0, 2, 2); // 6
      pass(0, 1, 1, 2); // 7
    } else {
      pass(0, 0, 1, 1);
    }
    return fullPixels;
  };

  PNG.prototype.decodePalette = function() {
    var c, i, length, palette, pos, ret, transparency, _i, _ref, _ref1;
    palette = this.palette;
    transparency = this.transparency.indexed || [];
    ret = new Uint8Array((transparency.length || 0) + palette.length);
    pos = 0;
    length = palette.length;
    c = 0;
    for (i = _i = 0, _ref = length; _i < _ref; i = _i += 3) {
      ret[pos++] = palette[i];
      ret[pos++] = palette[i + 1];
      ret[pos++] = palette[i + 2];
      ret[pos++] = (_ref1 = transparency[c++]) != null ? _ref1 : 255;
    }
    return ret;
  };

  PNG.prototype.copyToImageData = function(imageData, pixels) {
    var alpha, colors, data, i, input, j, k, length, palette, v, _ref;
    colors = this.colors;
    palette = null;
    alpha = this.hasAlphaChannel;
    if (this.palette.length) {
      palette =
        (_ref = this._decodedPalette) != null
          ? _ref
          : (this._decodedPalette = this.decodePalette());
      colors = 4;
      alpha = true;
    }
    data = imageData.data || imageData;
    length = data.length;
    input = palette || pixels;
    i = j = 0;
    if (colors === 1) {
      while (i < length) {
        k = palette ? pixels[i / 4] * 4 : j;
        v = input[k++];
        data[i++] = v;
        data[i++] = v;
        data[i++] = v;
        data[i++] = alpha ? input[k++] : 255;
        j = k;
      }
    } else {
      while (i < length) {
        k = palette ? pixels[i / 4] * 4 : j;
        data[i++] = input[k++];
        data[i++] = input[k++];
        data[i++] = input[k++];
        data[i++] = alpha ? input[k++] : 255;
        j = k;
      }
    }
  };

  PNG.prototype.decode = function() {
    var ret;
    ret = new Uint8Array(this.width * this.height * 4);
    this.copyToImageData(ret, this.decodePixels());
    return ret;
  };

  var hasBrowserCanvas = function() {
    if (Object.prototype.toString.call(globalObject) === "[object Window]") {
      try {
        scratchCanvas = globalObject.document.createElement("canvas");
        scratchCtx = scratchCanvas.getContext("2d");
      } catch (e) {
        return false;
      }
      return true;
    }
    return false;
  };

  hasBrowserCanvas();

  makeImage = function(imageData) {
    if (hasBrowserCanvas() === true) {
      var img;
      scratchCtx.width = imageData.width;
      scratchCtx.height = imageData.height;
      scratchCtx.clearRect(0, 0, imageData.width, imageData.height);
      scratchCtx.putImageData(imageData, 0, 0);
      img = new Image();
      img.src = scratchCanvas.toDataURL();
      return img;
    }
    throw new Error("This method requires a Browser with Canvas-capability.");
  };

  PNG.prototype.decodeFrames = function(ctx) {
    var frame, i, imageData, pixels, _i, _len, _ref, _results;
    if (!this.animation) {
      return;
    }
    _ref = this.animation.frames;
    _results = [];
    for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
      frame = _ref[i];
      imageData = ctx.createImageData(frame.width, frame.height);
      pixels = this.decodePixels(new Uint8Array(frame.data));
      this.copyToImageData(imageData, pixels);
      frame.imageData = imageData;
      _results.push((frame.image = makeImage(imageData)));
    }
    return _results;
  };

  PNG.prototype.renderFrame = function(ctx, number) {
    var frame, frames, prev;
    frames = this.animation.frames;
    frame = frames[number];
    prev = frames[number - 1];
    if (number === 0) {
      ctx.clearRect(0, 0, this.width, this.height);
    }
    if (
      (prev != null ? prev.disposeOp : void 0) === APNG_DISPOSE_OP_BACKGROUND
    ) {
      ctx.clearRect(prev.xOffset, prev.yOffset, prev.width, prev.height);
    } else if (
      (prev != null ? prev.disposeOp : void 0) === APNG_DISPOSE_OP_PREVIOUS
    ) {
      ctx.putImageData(prev.imageData, prev.xOffset, prev.yOffset);
    }
    if (frame.blendOp === APNG_BLEND_OP_SOURCE) {
      ctx.clearRect(frame.xOffset, frame.yOffset, frame.width, frame.height);
    }
    return ctx.drawImage(frame.image, frame.xOffset, frame.yOffset);
  };

  PNG.prototype.animate = function(ctx) {
    var doFrame,
      frameNumber,
      frames,
      numFrames,
      numPlays,
      _ref,
      _this = this;
    frameNumber = 0;
    (_ref = this.animation),
      (numFrames = _ref.numFrames),
      (frames = _ref.frames),
      (numPlays = _ref.numPlays);
    return (doFrame = function() {
      var f, frame;
      f = frameNumber++ % numFrames;
      frame = frames[f];
      _this.renderFrame(ctx, f);
      if (numFrames > 1 && frameNumber / numFrames < numPlays) {
        return (_this.animation._timeout = setTimeout(doFrame, frame.delay));
      }
    })();
  };

  PNG.prototype.stopAnimation = function() {
    var _ref;
    return clearTimeout(
      (_ref = this.animation) != null ? _ref._timeout : void 0
    );
  };

  PNG.prototype.render = function(canvas) {
    var ctx, data;
    if (canvas._png) {
      canvas._png.stopAnimation();
    }
    canvas._png = this;
    canvas.width = this.width;
    canvas.height = this.height;
    ctx = canvas.getContext("2d");
    if (this.animation) {
      this.decodeFrames(ctx);
      return this.animate(ctx);
    } else {
      data = ctx.createImageData(this.width, this.height);
      this.copyToImageData(data, this.decodePixels());
      return ctx.putImageData(data, 0, 0);
    }
  };

  return PNG;
})();

export { PNG };
